using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;

namespace Brilliance.utilities
{
    public class HandlesAttribute : Attribute
    {
        public readonly string DelegateName;

        public HandlesAttribute(string delegateName)
        {
            DelegateName = delegateName;
        }
    }

    public sealed class ReflectUtil
    {
        private static readonly BindingFlags PrivatePublicStaticInstance = BindingFlags.NonPublic | BindingFlags.Public |
                                                                           BindingFlags.Instance | BindingFlags.Static;

        public delegate bool MatchesOnDelegate(Delegate subject);

        public static Type[] FindTypesWithCustomAttribute(Assembly asm, Type attributeType, bool inherit)
        {
            var matchingTypes = new List<Type>();
            var types = asm.GetTypes();
            foreach (var type in types)
            {
                var attributes = type.GetCustomAttributes(attributeType, inherit);
                if (attributes.Length > 0)
                {
                    matchingTypes.Add(type);
                }
            }
            return matchingTypes.ToArray();
        }

        public static void RemoveEventHandlersFrom(MatchesOnDelegate matchesOnDelegate,
                                                   params object[] objectsWithEvents)
        {
            foreach (var owningObject in objectsWithEvents)
            {
                if (owningObject != null)
                {
                    foreach (var eventInfo in GetEvents(owningObject))
                    {
                        var handlesAttributes = eventInfo.GetCustomAttributes(typeof (HandlesAttribute), true);

                        if (handlesAttributes.Length > 0)
                        {
                            var attribute = (HandlesAttribute) handlesAttributes[0];
                            var eventDelegateName = attribute.DelegateName;
                            var eventDelegateField = owningObject.GetType().GetField(eventDelegateName,
                                                                                     PrivatePublicStaticInstance);
                            var eventDelegate = (Delegate) eventDelegateField.GetValue(owningObject);

                            if (eventDelegate != null)
                            {
                                foreach (var subscriber in eventDelegate.GetInvocationList())
                                {
                                    if (matchesOnDelegate(subscriber))
                                    {
                                        RemoveSubscriberEvenIfItsPrivate(eventInfo, owningObject, subscriber);
                                    }
                                }
                            }
                        }
                    }

                    foreach (var eventFromOwningObject in GetDelegates(owningObject))
                    {
                        foreach (var subscriber in eventFromOwningObject.GetInvocationList())
                        {
                            if (matchesOnDelegate(subscriber))
                            {
                                var theEvent = eventFromOwningObject.GetEventInfo();

                                if (theEvent != null)
                                {
                                    RemoveSubscriberEvenIfItsPrivate(theEvent, owningObject, subscriber);
                                }
                            }
                        }
                    }
                }
            }
        }

        private static IEnumerable<EventInfo> GetEvents(object owningObject)
        {
            var type = owningObject.GetType();
            var events = new List<EventInfo>();
            while (type != null)
            {
                var eventInfos = type.GetEvents();
                events.AddRange(eventInfos);
                type = type.BaseType;
            }
            return events;
        }

        // You can use eventInfo.RemoveEventHandler(owningObject, subscriber)
        // unless it's a private delegate
        private static void RemoveSubscriberEvenIfItsPrivate(EventInfo eventInfo, object owningObject,
                                                             Delegate subscriber)
        {
            var privateRemoveMethod = eventInfo.GetRemoveMethod(true);
            privateRemoveMethod.Invoke(owningObject, PrivatePublicStaticInstance, null, new object[] {subscriber},
                                       CultureInfo.CurrentCulture);
        }

        private static DelegateInfo[] GetDelegates(object owningObject)
        {
            var type = owningObject.GetType();
            var delegates = new List<DelegateInfo>();
            while (type != null)
            {
                var fieldInfos = type.GetFields(PrivatePublicStaticInstance);
                foreach (var fieldInfo in fieldInfos)
                {
                    var eventFromOwningObject = fieldInfo.GetValue(owningObject) as Delegate;
                    if (eventFromOwningObject != null)
                    {
                        delegates.Add(new DelegateInfo(eventFromOwningObject, fieldInfo, owningObject));
                    }
                }
                type = type.BaseType;
            }
            return delegates.ToArray();
        }

        private class DelegateInfo
        {
            private readonly Delegate delegateInformation;
            private readonly FieldInfo fieldInfo;
            private readonly object owningObject;

            public DelegateInfo(Delegate delegateInformation, FieldInfo fieldInfo, object owningObject)
            {
                this.delegateInformation = delegateInformation;
                this.fieldInfo = fieldInfo;
                this.owningObject = owningObject;
            }

            public Delegate[] GetInvocationList()
            {
                return delegateInformation.GetInvocationList();
            }

            public EventInfo GetEventInfo()
            {
                return owningObject.GetType().GetEvent(fieldInfo.Name, PrivatePublicStaticInstance);
            }
        }
    }
}